package lexer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;

/**
 * iterates through all characters in a file of logo code and tokenizes it
 * 
 * @author Caroline
 *
 */
public class Lexer {

	ArrayList<String> fileLines = null;
	LinkedList<Token> tokens = new LinkedList<Token>();

	/**
	 * constructor reads in the file
	 * @param fileName
	 */
	public Lexer(String fileName) {
		try {
			fileLines = LogoReader.readLines(fileName);
		} catch (IOException e) {
			// File could not be read
			System.out.println("Fatal Error: Unable to read " + e.getMessage() + " for writing.");
			System.exit(2);
		}
	}
	
	/**
	 * iterates through an array list checking every character to be either a white space, a symbol or part
	 * of an operator and groups the characters inbetween and tokenizes all, but white space
	 * 
	 */
	private void lex() {
		
		String line;
		char current = ' ';
		String characters = "";
		Token token = null;
		Token operatorToken = null;
		Boolean isOperator = false;
		int fileLine = 0;
		int fileCol = 0;

		// iterate through all characters in the file
		for (int i = 1; i <= fileLines.size(); i++) {
			line = fileLines.get(i - 1);
			for (int j = 0; j <= line.length(); j++) {
								
				if(isOperator){
					
					// reads operator
					String operator = "" + current;
					char next;
					if(fileCol <= line.length()){
						next = line.charAt(fileCol);
						if(next == '=') {
							operator += '=';
							j++;
						}
					}
					
					// tokenises operator
					operatorToken = tokenizeOperator(operator, fileLine, fileCol);
					
					// tokenises text before operator
					if (characters.length() > 0) {
						token = tokenizeText(characters, fileLine, fileCol);
						addToken(token);
					}
					addToken(operatorToken);
										
					// reset to default values
					isOperator = false;
					characters = "";
					
				} else {
					// adds a space to the start of every line to make it's length greater than 0 and force the loop to execute at least once
					if(j == 0){
						current =  ' ';
					} else {
					// reads the current character
					// 1 get's subracted because a string starts at character 0, where as a line in a text editor starts at 1
					current = line.charAt(j - 1);
					}
					// in the case of white space, any set of characters before it get tokenized
					if (current == ' ' || current == '\t' || current == '\n' || current == '\r') {
						 if (characters.length() > 0) {
							token = tokenizeText(characters, fileLine, fileCol);
							addToken(token);
						 }
						 characters = "";
					} else { 
						
						// checks if the character itself represents a token
						operatorToken = tokenizeIfCharacterIsASymbol(current, fileLine, fileCol);
						if (operatorToken != null) {
							if(characters.length() > 0) {
								token = tokenizeDataType(characters, fileLine, fileCol);
								addToken(token);
								characters = "";
							}
							addToken(operatorToken);
						}
						
						// checks if the character is part of an operator
						else if (current == '=' || current == '!' || current == '<' || current == '>') {
							isOperator = true;
						
						} else {
							characters += current;
						}	
					}
				}
				fileLine = i;
				fileCol = j;
			}
		}
	}
	
	public Token[] lexFile() {
		this.lex();
		ErrorMessageCreator messageCreator = new ErrorMessageCreator(fileLines);
		tokens = messageCreator.addErrorMessagesToTokens(tokens);
		return tokens.toArray(new Token[tokens.size()]);
	}
	
	/**
	 * checks if a character is a one character symbol, that can only stand on it's own
	 * 
	 * @param text
	 * @param fileLine
	 * @param fileCol
	 * @return token of the appropriate kind
	 */
	private Token tokenizeIfCharacterIsASymbol(char text, int fileLine, int fileCol) {
		String symbol = "" + text;
		switch (symbol) {
		case "(":
			return new Token(TokenType.LEFT_BRACKET, fileLine, fileCol);
		case ")":
			return new Token(TokenType.RIGHT_BRACKET, fileLine, fileCol);
		case "+":
			return new Token(TokenType.ADD, fileLine, fileCol);
		case "-":
			return new Token(TokenType.SUBTRACT, fileLine, fileCol);
		case "*":
			return new Token(TokenType.MULTIPLY, fileLine, fileCol);
		case "/":
			return new Token(TokenType.DIVIDE, fileLine, fileCol);
		default:
			return null;
		}
	}
	
	/**
	 * adds a token to a list of tokens given that it is not equal to null
	 * 
	 * @param token
	 */
	private void addToken(Token token) {
		if(token != null) {
			tokens.add(token);
		}
	}
	
	/**
	 * tokenizes a set of characters as a keywork a number or an identifier 
	 * 
	 * @param text
	 * @param fileLine
	 * @param fileCol
	 * @return token of the appropriate kind
	 */
	private Token tokenizeText (String text, int fileLine, int fileCol) {
		Token token = null;
		token = tokenizeKeyword(text, fileLine, fileCol);
		if (token == null) {
			token = tokenizeDataType(text, fileLine, fileCol);
		}
		return token;
	}
	
	/**
	 * checks a group of characters against being a keyword and tokenizes it
	 * in case of a math
	 * 
	 * @param text
	 * @param fileLine
	 * @param fileCol
	 * @return token of the appropriate kind
	 */
	private Token tokenizeKeyword (String text, int fileLine, int fileCol) {
		switch (text) {
		case "PROC":
			return new Token(TokenType.BEGIN_PROC, fileLine, fileCol);
		case "IF":
			return new Token(TokenType.IF, fileLine, fileCol);
		case "THEN":
			return new Token(TokenType.THEN, fileLine, fileCol);
		case "ELSE":
			return new Token(TokenType.ELSE, fileLine, fileCol);
		case "ENDIF":
			return new Token(TokenType.ENDIF, fileLine, fileCol);
		case "FORWARD":
			return new Token(TokenType.FORWARD, fileLine, fileCol);
		case "LEFT":
			return new Token(TokenType.LEFT, fileLine, fileCol);
		case "RIGHT":
			return new Token(TokenType.RIGHT, fileLine, fileCol);
		case "SETRED":
			return new Token(TokenType.SETRED, fileLine, fileCol);
		case "SETGREEN":
			return new Token(TokenType.SETGREEN, fileLine, fileCol);
		case "SETBLUE":
			return new Token(TokenType.SETBLUE, fileLine, fileCol);
		case "SETSTROKEWIDTH":
			return new Token(TokenType.SETSTROKEWIDTH, fileLine, fileCol);
		default:
			return null;
		}
	}
	
	/**
	 * tokenizes operators that consist out of 2 characters or could potentially have a second one behind them
	 * 
	 * @param operator
	 * @param fileLine
	 * @param fileCol
	 * @return token of the appropriate kind
	 */
	private Token tokenizeOperator (String operator, int fileLine, int fileCol) {
		switch (operator) {
		case "==":
			return new Token(TokenType.EQUALS, fileLine, fileCol);
		case "!=":
			return new Token(TokenType.NOT_EQUALS, fileLine, fileCol);
		case "<":
			return new Token(TokenType.LESS_THAN, fileLine, fileCol);
		case ">":
			return new Token(TokenType.GREATER_THAN, fileLine, fileCol);
		case "<=":
			return new Token(TokenType.LESS_THAN_OR_EQUAL, fileLine, fileCol);
		case ">=":
			return new Token(TokenType.GREATER_THAN_OR_EQUAL, fileLine, fileCol);
		default:
			System.out.println("An error occured in line " + fileLine + " when trying to tokenise an operator");
			return null;
		}
	}
	
	/**
	 * tokenizes a set of characters as a number or an identifier 
	 * 
	 * @param text
	 * @param fileLine
	 * @param fileCol
	 * @return token of the appropriate kind
	 */
	private Token tokenizeDataType(String text, int fileLine, int fileCol) {
		
		Token token = null;
		text = text.trim();
		
		// if the character starts on a digit it can't be an identifier and must be a number
		if(Character.isDigit(text.charAt(0))) {
			boolean digit = true;
			// checks if all other characters are also digits
			for (int i = 0; (i < text.length()) && digit; i++) {
				if(!Character.isDigit(text.charAt(i))) {
					digit = false;
				}
			}
			// providing all other characters in the number are digits, it get's tokenized as a digit
			if(digit) {
				token = new Token(TokenType.NUMBER, fileLine, fileCol, text);
				return token;
			}
		// if the first character is a letter, it gets tokenized as an identifier
		} else {
			if(Character.isLetter(text.charAt(0))) {
				token = new Token(TokenType.IDENTIFIER, fileLine, fileCol, text);
				return token;
			}
		}
		return token;
	}
}
